// Gra RATMAN
// W ramach projektu stworzono autorską grę zręcznościową, która czerpie swoje inspiracje z szerokiego świata gier arcade i nosi doniosłą nazwę RatMAN. Gra została wgrana na moduł Arduino Uno oraz napisana w języku C++ przy użyciu własnych zdolności intelektualnych oraz pomocy sztucznej inteligencji.
// Autorzy:
// Teresa Franc Michał Tyborowski


// Wymagane biblioteki
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

#define i2c_Address 0x3c
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1

Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Przyciski
#define PIN_LEFT_BTN 7
#define PIN_RESTART_BTN 8
#define PIN_RIGHT_BTN 9
#define PIN_DOWN_BTN 10

// --- GRAFIKA SZCZURKA (lewo) ---
const unsigned char epd_bitmap_pixil_frame_0 [] PROGMEM = {
0xdf, 0xff, 0xf0, 0xef, 0xff, 0xf0, 0xdf, 0xfd, 0xf0, 0xbc, 0x0c, 0xf0, 0xb1, 0xe4, 0xf0,
0xa7, 0xf3, 0x70, 0xaf, 0xfe, 0xb0, 0xcf, 0xf3, 0xd0, 0xcf, 0x84, 0x20, 0xe6, 0x37, 0xf0,
0xf1, 0x7b, 0xf0, 0xfd, 0xbf, 0xf0
};

// --- GRAFIKA SZCZURKA (prawo) ---
const unsigned char epd_bitmap_pixil_frame_0_mirrored [] PROGMEM = {
0xff, 0xff, 0xb0, 0xff, 0xff, 0x70, 0xfb, 0xff, 0xb0, 0xf3, 0x03, 0xd0, 0xf2, 0x78, 0xd0,
0xec, 0xfe, 0x50, 0xd7, 0xff, 0x50, 0xbc, 0xff, 0x30, 0x42, 0x1f, 0x30, 0xfe, 0xc6, 0x70,
0xfd, 0xe8, 0xf0, 0xff, 0xdb, 0xf0
};

// Wymiary szczurka
const int szczurekW = 20; // szerokość 
const int szczurekH = 12; // wysokość

// --- GRAFIKA BONUSOWEGO SERA ---
const unsigned char epd_bitmap_bonus_cheese [] PROGMEM = {
0x00, 0x30, 0x00, 0x64, 0x03, 0x0c, 0x0c, 0x66, 0x30, 0x92, 0xc0, 0x62, 0xff, 0xfe, 0x81, 0x82,
0xc2, 0x42, 0x21, 0x82, 0x20, 0x32, 0xc0, 0x4a, 0x88, 0x32, 0x94, 0x02, 0xff, 0xfe
};

// --- GRAFIKA CZASZKI ---
const unsigned char epd_bitmap_skull [] PROGMEM = {
0x07, 0xc0, 0x38, 0x38, 0x40, 0x04, 0x94, 0x52, 0x88, 0x22, 0x94, 0x52, 0x80, 0x02, 0x80, 0x82,
0x42, 0x84, 0x30, 0x18, 0x10, 0x10, 0x08, 0x20, 0x0a, 0xa0, 0x0a, 0xa0, 0x05, 0x40
};

// Ustawienia gry
const int SEG_HEIGHT = 24; // wysokość segmentu sera
const int NUM_SEGMENTS = 6; // numer segmentów wyświetlanych na ekranie
const int cheeseWidth = 15; // szerokość sera
const int cheeseX = (64 - cheeseWidth) / 2; // wyśrodkowanie sera

int segY[NUM_SEGMENTS];
int forkType[NUM_SEGMENTS];
int holeOffsetX[NUM_SEGMENTS];
int holeOffsetY[NUM_SEGMENTS];

// --- LOGIKA CZASU, PRĘDKOŚCI I PUNKTÓW ---
unsigned long gameStartTime = 0;
unsigned long lastMoveTime = 0;
unsigned long lastSpeedUpdateTime = 0;
int score = 0;

// Prędkość 
float currentMoveInterval = 5.0;
const float minInterval = 1.0;
int dropSpeed = 1; // Domyślnie Easy

// Pozycjonowanie szczurka i zjadanie sera
bool isRatOnLeft = true; // Start gry z lewej strony
const int ratY = 100; // Połozenie szczurka na ekranie
const int eatingLine = ratY + 6;
const int disappearLine = ratY + szczurekH + 3; // Znikanie sera 3 piksele nad szczurkiem

bool gameOver = false;
bool showStartScreen = true;
int menuSelection = 1; // 1 = Easy, 2 = Hard, 3 = Extreme

// --- ZMIENNE DO KONTROLOWANEJ LOSOWOŚCI ---
int lastForkType = -1;
int consecutiveForks = 0;
int forksSinceLastBonus = 0;
int nextBonusTarget = 15;
int scoreAtLastSkull = 0;
int nextSkullTarget = 20;

void setup() {
display.begin(i2c_Address, true);
display.setRotation(3);
pinMode(PIN_LEFT_BTN, INPUT_PULLUP);
pinMode(PIN_RESTART_BTN, INPUT_PULLUP);
pinMode(PIN_RIGHT_BTN, INPUT_PULLUP);
pinMode(PIN_DOWN_BTN, INPUT_PULLUP);
randomSeed(analogRead(0));
resetGame();
}

// Funkcja losowości pojawiania się widelcy i bonusów
void randomizeSegment(int index, bool isInitial = false) {
if (isInitial) {
forkType[index] = 2; // Brak widelca na start
} else { // Ujemna czaszka i bonusowy ser
forksSinceLastBonus++;
if (score >= scoreAtLastSkull + nextSkullTarget) {
int side = random(0, 2);
forkType[index] = (side == 0) ? 5 : 6; // Czaszka lewo 5, prawo 6
scoreAtLastSkull = score; // Licznik punktacji
nextSkullTarget = random(20, 31); // Losowość pojawiania się czaszki
}
else if (forksSinceLastBonus >= nextBonusTarget) {
int bonusSide = random(0, 2);
forkType[index] = (bonusSide == 0) ? 3 : 4; // Ser lewo 3, prawo 4
forksSinceLastBonus = 0; // Liczniki widelcy
nextBonusTarget = random(10, 18); // Losowość pojawiania się bonusowego sera
}
else {
int nextFork = random(0, 2); // Losowanie połozenia widelca 0 (lewo) lub 1 (prawo)
// Sprawdzamy, czy wylosowaliśmy tę samą stronę co poprzednio
if (nextFork == lastForkType) {
consecutiveForks++;
// Jeśli to już 4 raz z rzędu na tej samej stronie, wymuszamy zmianę
if (consecutiveForks >= 4) {
nextFork = 1 - nextFork; // 1-0=1, 1-1=0 (zmienia stronę)
consecutiveForks = 1; // Resetujemy licznik dla nowej strony
}
} else {
// Jeśli strona jest inna, resetujemy licznik
consecutiveForks = 1;
}
lastForkType = nextFork;
forkType[index] = nextFork;
}
}
holeOffsetX[index] = random(2, cheeseWidth - 4);
holeOffsetY[index] = random(2, SEG_HEIGHT - 4);
}

// Restartowanie gry
void resetGame() {
// Resetujemy liczniki powtarzalności przed każdą grą
lastForkType = -1;
consecutiveForks = 0;
forksSinceLastBonus = 0;
nextBonusTarget = random(10, 18);
scoreAtLastSkull = 0;
nextSkullTarget = random(20, 31);

for (int i = 0; i < NUM_SEGMENTS; i++) {
segY[i] = (i * SEG_HEIGHT) - SEG_HEIGHT;
randomizeSegment(i, true);
}
gameOver = false;
isRatOnLeft = true;
score = 0;
// Różne prędkości startowe dla poziomów trudności
if (dropSpeed == 1) currentMoveInterval = 15.0; // Easy
else if (dropSpeed == 2) currentMoveInterval = 10.0; // Hard
else currentMoveInterval = 5.0; // Extreme
gameStartTime = millis();
lastMoveTime = millis();
lastSpeedUpdateTime = millis();
}

// Funkcja rysowania widelca
void drawFork(int x, int y, bool isLeft) {
int direction = isLeft ? -1 : 1;
display.drawLine(x, y, x + (direction * 10), y, SH110X_WHITE);
display.drawLine(x + (direction * 10), y, x + (direction * 14), y - 2, SH110X_WHITE);
display.drawLine(x + (direction * 10), y, x + (direction * 14), y + 2, SH110X_WHITE);
}

void loop() {
// --- EKRAN STARTOWY ---
if (showStartScreen) {
display.clearDisplay();
// logo
display.drawBitmap(22, 10, epd_bitmap_pixil_frame_0, szczurekW, szczurekH, SH110X_BLACK, SH110X_WHITE);
display.setTextSize(1);
display.setTextColor(SH110X_WHITE);
// nazwa
display.setCursor(14, 25);
display.print("RATMAN");
// menu wyboru - poziomy trudności
display.setCursor(5, 45);
if (menuSelection == 1) display.print("> EASY"); else display.print(" EASY");
display.setCursor(5, 57);
if (menuSelection == 2) display.print("> HARD"); else display.print(" HARD");
display.setCursor(5, 69);
if (menuSelection == 3) display.print("> EXTREME"); else display.print(" EXTREME");
display.setCursor(18, 90);
display.print("Press");
display.setCursor(18, 100);
display.print("RIGHT");
display.setCursor(10, 110);
display.print("to play");
display.display();
// obsługa przycisków
if (digitalRead(PIN_RESTART_BTN) == LOW) {
menuSelection--;
if (menuSelection < 1) menuSelection = 3;
delay(150);
}
if (digitalRead(PIN_DOWN_BTN) == LOW) {
menuSelection++;
if (menuSelection > 3) menuSelection = 1;
delay(150);
}

if (digitalRead(PIN_RIGHT_BTN) == LOW) {
if (menuSelection == 1) dropSpeed = 1;
else if (menuSelection == 2) dropSpeed = 2;
else dropSpeed = 3;
delay(200);
showStartScreen = false;
resetGame();
}
return;
}

// --- LOGIKA POWROTU DO MENU PO PRZEGRANEJ ---
if (gameOver && digitalRead(PIN_RESTART_BTN) == LOW) {
delay(200);
showStartScreen = true;
return;
}

if (!gameOver && digitalRead(PIN_RESTART_BTN) == LOW) {
delay(200);
showStartScreen = true;
return;
}

if (!gameOver) {
if (digitalRead(PIN_LEFT_BTN) == LOW) isRatOnLeft = true;
if (digitalRead(PIN_RIGHT_BTN) == LOW) isRatOnLeft = false;

unsigned long currentTime = millis();
unsigned long playTime = currentTime - gameStartTime;

if (playTime > 2000) {
if (currentTime - lastSpeedUpdateTime >= 10) {
lastSpeedUpdateTime = currentTime;
if (currentMoveInterval > minInterval) {
currentMoveInterval -= 2;
}
}
}

if (currentTime - lastMoveTime >= (unsigned long)currentMoveInterval) {
lastMoveTime = currentTime;

for (int i = 0; i < NUM_SEGMENTS; i++) {
segY[i] += dropSpeed;
if (segY[i] >= 150) {
segY[i] -= (NUM_SEGMENTS * SEG_HEIGHT);
randomizeSegment(i, false);
}

if (forkType[i] != 2) { // jeśli jest widelec, czaszka lub ser
int forkY = segY[i] + (SEG_HEIGHT / 2);
int targetLine = ratY + szczurekH + 1;
if (forkY >= targetLine && forkY < targetLine + dropSpeed) {
// licznik ominiętych widelcy (punktacja)
if (forkType[i] == 0 || forkType[i] == 1) score++;
}

// Warunki przegrania gry i punktacji
if (forkY >= ratY && forkY <= ratY + szczurekH) {
bool ratLeft = isRatOnLeft;
// Zderzenie z widelcem
if (forkType[i] == 0 && ratLeft) gameOver = true;
if (forkType[i] == 1 && !ratLeft) gameOver = true;
// Punktacja bonusowego sera (+10)
if (forkType[i] == 3 && ratLeft) { score += 10; forkType[i] = 2; }
if (forkType[i] == 4 && !ratLeft) { score += 10; forkType[i] = 2; }
// Punktacja czaszki (-20)
if (forkType[i] == 5 && ratLeft) {
score -= 20; forkType[i] = 2;
// Ujemna punktacja po złapaniu czaszki - GAME OVER
if (score < 0) gameOver = true;
}
if (forkType[i] == 6 && !ratLeft) {
score -= 20; forkType[i] = 2;
// Ujemna punktacja po złapaniu czaszki - GAME OVER
if (score < 0) gameOver = true;
}
}
}
}
}
}

// --- RYSOWANIE GRY ---
display.clearDisplay();

for (int i = 0; i < NUM_SEGMENTS; i++) {
int y = segY[i];
int visibleHeight = SEG_HEIGHT;
if (y + SEG_HEIGHT > eatingLine) visibleHeight = eatingLine - y;
// Ser na środku ekranu
if (visibleHeight > 0 && y < 128) {
display.fillRect(cheeseX, y, cheeseWidth, visibleHeight, SH110X_WHITE);
int holeY = y + holeOffsetY[i];
if (holeY < eatingLine) display.fillCircle(cheeseX + holeOffsetX[i], holeY, 2, SH110X_BLACK);
}
// Widelce i bonusy
if (forkType[i] != 2 && y < 128 && y > -SEG_HEIGHT) {
int forkY = y + (SEG_HEIGHT / 2);
if (forkY <= disappearLine) {
if (forkType[i] == 0) drawFork(cheeseX, forkY, true);
else if (forkType[i] == 1) drawFork(cheeseX + cheeseWidth, forkY, false);
else if (forkType[i] == 3) display.drawBitmap(cheeseX - 20, forkY - 7, epd_bitmap_bonus_cheese, 15, 15, SH110X_WHITE);
else if (forkType[i] == 4) display.drawBitmap(cheeseX + cheeseWidth + 5, forkY - 7, epd_bitmap_bonus_cheese, 15, 15, SH110X_WHITE);
else if (forkType[i] == 5) display.drawBitmap(cheeseX - 20, forkY - 7, epd_bitmap_skull, 15, 15, SH110X_WHITE);
else if (forkType[i] == 6) display.drawBitmap(cheeseX + cheeseWidth + 5, forkY - 7, epd_bitmap_skull, 15, 15, SH110X_WHITE);
}
}
}

// Szczurek
int szczurekX = isRatOnLeft ? (cheeseX - szczurekW - 3) : (cheeseX + cheeseWidth + 3);
const unsigned char* currentBitmap = isRatOnLeft ? epd_bitmap_pixil_frame_0 : epd_bitmap_pixil_frame_0_mirrored;
display.drawBitmap(szczurekX, ratY, currentBitmap, szczurekW, szczurekH, SH110X_BLACK, SH110X_WHITE);

// Punktacja
if (!gameOver) {
display.setTextSize(1);
display.setTextColor(SH110X_WHITE);
display.setCursor(15, 120);
display.print("Pkt: ");
display.print(score);
}

// Ekran GAME OVER
if (gameOver) {
display.fillRect(0, 42, 64, 40, SH110X_BLACK);
display.setCursor(10, 45);
display.print("LOOSER");
display.setCursor(5, 57);
display.print("SCORE: ");
display.print(score);

display.setCursor(5, 69);
display.print("NEW GAME");
int arrX = 52; int arrY = 69;
display.drawLine(arrX + 2, arrY, arrX, arrY + 2, SH110X_WHITE);
display.drawLine(arrX + 2, arrY, arrX + 4, arrY + 2, SH110X_WHITE);
display.drawLine(arrX + 2, arrY, arrX + 2, arrY + 4, SH110X_WHITE);
}

display.display();
}